package cn.car.service.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.sql.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Component
@Slf4j
public class SchemaSync implements ApplicationRunner {

    @Autowired
    private SystemConfig systemConfig;

    @Autowired
    private DataSource dataSource;

    private static final Set<String> INT_FAMILY = new HashSet<>(
            Arrays.asList("tinyint", "smallint", "mediumint", "int", "integer", "bigint")
    );


    //PathMatchingResourcePatternResolver 是 Spring 提供的一个 资源加载器
    //可以用前缀（classpath:, classpath*:, file: 等）来指定资源位置；
    //返回的结果是 Spring 的 Resource[]，可以直接访问输入流。
    private final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 如果配置里 schema 同步功能关闭了，直接返回，不做任何事
        if (!systemConfig.getSchemaSyncEnabled().get()) {
            log.info("Schema sync disabled by configuration.");
            return;
        }

        // 从 classpath 下的 schema 目录里加载所有的 .sql 文件（这些是标准表的 DDL 文件）
        Resource[] resources = resolver.getResources("classpath*:schema/*.sql");
        if (resources.length == 0) {
            log.info("No schema files found under classpath:schema/*.sql");
            return;
        }

        // 建立 JDBC 连接，准备访问当前数据库
        Connection conn = DataSourceUtils.getConnection(dataSource);
        try {
            // 获取当前数据库名（catalog）
            String catalog = conn.getCatalog();

            // 遍历每一个 schema 目录下的 .sql 文件
            for (Resource r : resources) {
                String filename = r.getFilename();
                // 不是 .sql 文件的直接跳过
                if (filename == null || !filename.toLowerCase().endsWith(".sql")) continue;

                // 去掉 .sql 后缀，得到表名
                String table = filename.substring(0, filename.length() - 4);

                // 读取文件里的 DDL 内容
                String ddl = readResource(r);
                if (ddl == null || ddl.isBlank()) {
                    log.warn("Empty DDL file for table {}", table);
                    continue;
                }

                // 调用 syncOneTable 对比标准 DDL 和数据库现有表结构，如果有差异就生成/执行 ALTER SQL
                try {
                    syncOneTable(conn, catalog, table, ddl);
                } catch (Throwable t) {
                    log.error("Failed to sync table {}: {}", table, t.getMessage(), t);
                }
            }
        } finally {
            // 无论如何最后都释放数据库连接
            DataSourceUtils.releaseConnection(conn, dataSource);
        }

        log.info("Schema sync finished.");
    }

    /**
     * 读取指定的 Spring Resource（例如 classpath 下的 .sql 文件），
     * 并将文件内容完整地转成一个字符串返回。
     *
     * @param r Spring Resource 对象（文件、类路径资源等）
     * @return 文件内容的字符串形式；如果读取失败则返回 null
     */
    private String readResource(Resource r) {
        try (
            // 用输入流读取资源文件，并包装成 BufferedReader 方便逐行读取
            BufferedReader br = new BufferedReader(new InputStreamReader(r.getInputStream()))
        ) {
            // 将文件的每一行通过 Stream API 拼接成一个完整的字符串（行之间用换行符连接）
            return br.lines().collect(Collectors.joining("\n"));
        } catch (Exception e) {
            // 出错时打印 warn 日志，并返回 null（避免抛出异常导致程序中断）
            log.warn("Failed to read resource {}: {}", r, e.getMessage());
            return null;
        }
    }

    // 同步单张表（只生成 ADD / MODIFY），并在配置允许时执行
    private void syncOneTable(Connection conn, String catalog, String table, String canonicalDDL) throws SQLException {
        log.info("Sync table: {} (catalog={})", table, catalog);


        Map<String, ColumnDef> target = parseColumnsFromDDL(canonicalDDL);
        if (target.isEmpty()) {
            log.warn("No columns parsed for table {}. skip.", table);
            return;
        }


        Map<String, ColumnDef> current = loadColumnsFromJdbc(conn, catalog, table);


        List<String> alterSqls = new ArrayList<>();


// 找出缺失或差异列
        for (Map.Entry<String, ColumnDef> e : target.entrySet()) {
            String col = e.getKey();
            ColumnDef tcol = e.getValue();
            ColumnDef ccol = current.get(col);
            if (ccol == null) {
// ADD
                String add = buildAddColumnSql(table, tcol);
                alterSqls.add(add);
            } else {
// 比较类型/nullable/default
                if (!normalizeType(ccol.type).equalsIgnoreCase(normalizeType(tcol.type))
                        || ccol.nullable != tcol.nullable
                        || !Objects.equals(normalizeDefault(ccol.defaultValue), normalizeDefault(tcol.defaultValue))) {
                    String mod = buildModifyColumnSql(table, tcol);
                    alterSqls.add(mod);
                }
            }
        }


// 记录多余列（不自动 drop）
        for (String col : current.keySet()) {
            if (!target.containsKey(col)) {
                log.warn("Table {} has extra column '{}' not present in canonical DDL. Not dropping automatically.", table, col);
            }
        }


        if (alterSqls.isEmpty()) {
            log.info("Table {} is up-to-date.", table);
            return;
        }


        log.info("Generated {} alter SQL(s) for table {}:", alterSqls.size(), table);
        alterSqls.forEach(log::info);


        //如果 dryRun 模式开启了，就只打印出要执行的 SQL，不真正改数据库。
        //dryRun=true 相当于“演练模式”，让你看到哪些表要改，但不会真的去执行 ALTER TABLE
        if (systemConfig.getSchemaSyncDryRun().get()) {
            log.info("Dry-run enabled. Not applying changes for table {}.", table);
            return;
        }


        //如果没有开启 autoApply，就即使 dryRun=false，也不会自动执行 SQL，只提示你“请手工执行”。
        //autoApply=false 表示“需要人工确认”，系统不会自动跑。
        //这一步是第二道保险，避免误操作。
        if (!systemConfig.getSchemaSyncAutoApply().get()) {
            log.info("Auto-apply disabled. Please review the above SQL and apply manually if desired.");
            return;
        }


// 执行备份（如果配置）
        try (Statement stmt = conn.createStatement()) {
            if (systemConfig.getSchemaSyncBackupBeforeApply().get()) {
                String backupName = table + "_backup_" + System.currentTimeMillis();
                String backupSql = String.format("CREATE TABLE `%s` LIKE `%s`", backupName, table);
                log.info("Creating backup table with SQL: {}", backupSql);
                stmt.execute(backupSql);
            }


            for (String sql : alterSqls) {
                log.info("Executing: {}", sql);
                stmt.execute(sql);
            }
            log.info("Applied schema changes for table {}.", table);
        }
    }


    private String normalizeType(String t) {
        if (t == null) return "";
        String s = t.trim().toLowerCase().replaceAll("\\s+", " ");
        // 把诸如 "bigint(20) unsigned" 分解为 base + params + unsigned
        // 提取 base type
        String base = s.replaceAll("\\s*\\(.*?\\)", "")       // 去掉括号部分看看基础类型
                .replaceAll("\\s+unsigned", "")        // 暂时剥离 unsigned
                .trim();

        // 是否 unsigned
        boolean unsigned = s.contains("unsigned");

        // 括号参数（如果有）
        String params = null;
        java.util.regex.Matcher pm = java.util.regex.Pattern.compile("\\((.*?)\\)").matcher(s);
        if (pm.find()) params = pm.group(1).trim();

        // 整型族：去掉显示宽度，不保留 (n)
        if (INT_FAMILY.contains(base)) {
            StringBuilder out = new StringBuilder(base);
            if (unsigned) out.append(" unsigned");
            return out.toString();
        }

        // decimal / numeric：需要保留 (p,s)
        if ("decimal".equals(base) || "numeric".equals(base)) {
            StringBuilder out = new StringBuilder(base);
            if (params != null && !params.isEmpty()) {
                out.append("(").append(params).append(")");
            }
            if (unsigned) out.append(" unsigned");
            return out.toString();
        }

        // 其他类型：原样保留括号参数
        StringBuilder out = new StringBuilder(base);
        if (params != null && !params.isEmpty()) {
            out.append("(").append(params).append(")");
        }
        if (unsigned) out.append(" unsigned");
        return out.toString();
    }


    private String normalizeDefault(String d) {
        if (d == null) return null;
        String x = d.trim().replaceAll("^'+|'+$", "").toLowerCase();
        if ("null".equals(x)) return null; // 关键：'null' 也视为 null
        return d.trim().replaceAll("^'+|'+$", ""); // 去掉引号
    }


    // 构造 ADD COLUMN
    private String buildAddColumnSql(String table, ColumnDef c) {
        String def = c.defaultValue != null ? " DEFAULT '" + escapeSingleQuote(c.defaultValue) + "'" : "";
        String notNull = c.nullable ? "" : " NOT NULL";
        return String.format("ALTER TABLE `%s` ADD COLUMN `%s` %s%s%s;", table, c.name, c.type, notNull, def);
    }

    private String escapeSingleQuote(String s) {
        return s == null ? null : s.replace("'", "\\'");
    }

    // 构造 MODIFY COLUMN（MySQL 语法）
    private String buildModifyColumnSql(String table, ColumnDef c) {
        String def = c.defaultValue != null ? " DEFAULT '" + escapeSingleQuote(c.defaultValue) + "'" : "";
        String notNull = c.nullable ? "" : " NOT NULL";
        return String.format("ALTER TABLE `%s` MODIFY COLUMN `%s` %s%s%s;", table, c.name, c.type, notNull, def);
    }

    /**
     * 从数据库中读取某张表的列定义信息。
     * 优先使用 MySQL 特有的 `SHOW COLUMNS` 语句（因为能拿到 unsigned、显示宽度等），
     * 如果失败再回退到 JDBC 的 DatabaseMetaData。
     *
     * @param conn    JDBC 连接
     * @param catalog 数据库名
     * @param table   表名
     * @return        key=小写列名, value=列定义 ColumnDef
     */
    private Map<String, ColumnDef> loadColumnsFromJdbc(Connection conn, String catalog, String table) {
        Map<String, ColumnDef> map = new LinkedHashMap<>();

        // -------------------------------
        // 1. 优先用 SHOW COLUMNS
        // -------------------------------
        // SHOW COLUMNS 能拿到更接近原始 DDL 的定义：
        //   - Type: "bigint(20) unsigned"
        //   - Null: YES/NO
        //   - Default: 默认值（可能是 null、字符串、函数）
        String showSql = "SHOW COLUMNS FROM `" + table + "`";
        try (Statement st = conn.createStatement();
             ResultSet rs = st.executeQuery(showSql)) {

            while (rs.next()) {
                String colName = rs.getString("Field");    // 列名
                String typeStr = rs.getString("Type");     // 列类型（包含 unsigned、显示宽度）
                String nullable = rs.getString("Null");    // 是否可空 (YES/NO)
                String def = rs.getString("Default");      // 默认值，可能是 null 或常量/函数

                // 封装成 ColumnDef
                ColumnDef c = new ColumnDef();
                c.name = colName;
                c.type = typeStr;                          // 保留原始 Type，后续再 normalize
                c.nullable = "YES".equalsIgnoreCase(nullable);
                c.defaultValue = def;

                // 存入 map（key 用小写，避免大小写差异）
                map.put(colName.toLowerCase(), c);
            }

            // 如果通过 SHOW COLUMNS 成功获取数据，就直接返回
            if (!map.isEmpty()) return map;
        } catch (Exception ignore) {
            // 如果数据库不支持 SHOW COLUMNS（比如非 MySQL），就忽略异常，进入回退逻辑
        }

        // -------------------------------
        // 2. 回退到 DatabaseMetaData
        // -------------------------------
        // DatabaseMetaData#getColumns 是标准 JDBC API，适配不同数据库。
        // 但它拿到的类型信息通常比较“简化”，可能没有 unsigned、显示宽度。
        try {
            DatabaseMetaData md = conn.getMetaData();
            try (ResultSet rs = md.getColumns(catalog, null, table, null)) {
                while (rs.next()) {
                    String colName = rs.getString("COLUMN_NAME");    // 列名
                    String typeName = rs.getString("TYPE_NAME");     // 列类型名（如 BIGINT、VARCHAR）
                    int size = rs.getInt("COLUMN_SIZE");             // 列长度/精度
                    String isNullable = rs.getString("IS_NULLABLE"); // YES/NO
                    String def = rs.getString("COLUMN_DEF");         // 默认值

                    // 拼接类型（比如 varchar(255)，但 int 不保留显示宽度）
                    String type = buildTypeWithSize(typeName, size);

                    // 封装成 ColumnDef
                    ColumnDef c = new ColumnDef();
                    c.name = colName;
                    c.type = type;
                    c.nullable = "YES".equalsIgnoreCase(isNullable);
                    c.defaultValue = def;

                    // 存入 map
                    map.put(colName.toLowerCase(), c);
                }
            }
        } catch (SQLException e) {
            log.warn("Failed to load columns for table {}: {}", table, e.getMessage());
        }

        return map;
    }



    private String buildTypeWithSize(String typeName, int size) {
        if (typeName == null) return "";
        String t = typeName.toLowerCase();

        if (INT_FAMILY.contains(t)) {
            // 整型不拼接显示宽度
            return t;
        }
        if ("text".equals(t) || "blob".equals(t)) {
            return t;
        }
        // 其他常见类型保留长度（varchar、char等）
        if (size > 0) {
            return t + "(" + size + ")";
        }
        return t;
    }


    /**
     * 从 DDL 字符串里解析出列定义（一个简化版的 parser，能覆盖大部分常见 MySQL CREATE TABLE 语句）。
     *
     * @param ddl 完整的建表 SQL，比如 SHOW CREATE TABLE 的输出
     * @return key=列名（小写），value=列定义 ColumnDef
     */
    private Map<String, ColumnDef> parseColumnsFromDDL(String ddl) {
        Map<String, ColumnDef> map = new LinkedHashMap<>();

        // 预处理：去掉注释、换行、多余空格，方便后面正则匹配
        String normalized = ddl.replaceAll("/\\*.*?\\*/", "") // 去掉 /* ... */ 形式的注释
                .replaceAll("\r\n", "\n")   // 统一换行符
                .replaceAll("\n", " ")      // 把换行替换成空格
                .replaceAll(" +", " ");     // 合并多个空格成一个

        // 找到 DDL 中括号里的部分（就是列定义和约束）
        int l = normalized.indexOf('(');
        int r = normalized.lastIndexOf(')');
        if (l < 0 || r < 0 || r <= l) return map;
        String inside = normalized.substring(l + 1, r);

        // 用自定义方法按逗号切分列定义，但要避免枚举/集合类型里的逗号 (比如 enum('a','b'))
        List<String> parts = splitDDLColumns(inside);

        // 用正则匹配列定义的基本格式：`列名` 列类型 ...
        Pattern colPattern = Pattern.compile("^`?([A-Za-z0-9_]+)`?\\s+([^,]+)$");

        // 遍历每一列或约束的定义
        for (String part : parts) {
            String line = part.trim();
            if (line.isEmpty()) continue;

            // 先过滤掉主键、唯一约束、普通索引、外键等，这些不是列
            String low = line.toLowerCase();
            if (low.startsWith("primary key") || low.startsWith("unique key")
                    || low.startsWith("key") || low.startsWith("constraint")
                    || low.startsWith("foreign key")) {
                continue; // 只解析列定义，忽略约束行
            }

            // 用正则提取列名和剩余部分
            Matcher m = colPattern.matcher(line);
            if (!m.find()) continue;
            String colName = m.group(1);   // 列名
            String rest = m.group(2).trim(); // 列的类型 + 约束部分

            // rest 以类型开始，后面可能跟着 NULL/NOT NULL, DEFAULT ...
            String type = firstTypeToken(rest); // 解析出类型（可能带括号，如 varchar(255)）

            boolean isUnsigned = rest.toLowerCase().contains("unsigned");

            // 关键：把 unsigned 修饰拼到类型中，便于后续对比与生成 SQL
            type = isUnsigned ? (type + " unsigned") : type;

            // 判断是否允许为空（如果包含 NOT NULL，则 nullable=false）
            boolean nullable = !rest.toLowerCase().contains("not null");

            // 解析 DEFAULT 默认值
            String defaultVal = null;
            int idx = rest.toLowerCase().indexOf("default");
            if (idx >= 0) {
                String tmp = rest.substring(idx + "default".length()).trim();
                // 如果是单引号包裹的默认值
                if (tmp.startsWith("'")) {
                    int idx2 = tmp.indexOf('\'', 1);
                    if (idx2 > 0) defaultVal = tmp.substring(1, idx2);
                }
                // 如果是双引号包裹的默认值
                else if (tmp.startsWith("\"")) {
                    int idx2 = tmp.indexOf('\"', 1);
                    if (idx2 > 0) defaultVal = tmp.substring(1, idx2);
                }
                // 否则就直接取第一个 token（比如数字 0，CURRENT_TIMESTAMP 等）
                else {
                    String[] t2 = tmp.split("\\s+", 2);
                    defaultVal = t2[0].replaceAll("[,;]$", ""); // 去掉结尾的逗号或分号
                }
            }

            // 构造 ColumnDef 对象保存解析结果
            ColumnDef cd = new ColumnDef();
            cd.name = colName;
            cd.type = type;
            cd.nullable = nullable;
            cd.defaultValue = defaultVal;

            // 存入 map（统一用小写列名作为 key）
            map.put(colName.toLowerCase(), cd);
        }

        return map;
    }



    /**
     * 从列定义字符串中提取出“类型的第一个 token”
     * 示例：
     *   "varchar(255) not null default ''" -> "varchar(255)"
     *   "int not null auto_increment"      -> "int"
     *   "decimal(10,2) unsigned"           -> "decimal(10,2)"
     *
     * @param rest 列定义剩余部分（例如 "varchar(255) not null default ..."）
     * @return 提取出来的类型 token（可能带括号）
     */
    private String firstTypeToken(String rest) {
        rest = rest.trim();               // 去掉前后空格
        StringBuilder sb = new StringBuilder();
        int i = 0;

        // 从开头逐字符扫描
        while (i < rest.length()) {
            char c = rest.charAt(i);
            sb.append(c);                 // 先把字符加进去

            if (c == '(') {
                // 如果遇到左括号，说明类型里带参数（如 varchar(255)、decimal(10,2)）
                int paren = 1;
                i++;
                // 循环读取，直到括号闭合
                while (i < rest.length() && paren > 0) {
                    char d = rest.charAt(i);
                    sb.append(d);
                    if (d == '(') paren++;     // 遇到新括号，嵌套+1
                    else if (d == ')') paren--; // 遇到右括号，嵌套-1
                    i++;
                }
                break; // 类型部分已完整取出，跳出循环
            }

            // 如果遇到空白字符，说明类型部分结束（如 "int not null"）
            if (Character.isWhitespace(c)) break;

            i++;
        }

        return sb.toString().trim(); // 返回最终拼好的类型
    }



    private List<String> splitDDLColumns(String inside) {
        List<String> parts = new ArrayList<>();
        StringBuilder cur = new StringBuilder();
        int paren = 0;
        boolean inSingle = false;
        boolean inDouble = false;
        for (int i = 0; i < inside.length(); i++) {
            char c = inside.charAt(i);
            if (c == '\'') inSingle = !inSingle;
            else if (c == '"') inDouble = !inDouble;
            else if (!inSingle && !inDouble) {
                if (c == '(') paren++;
                else if (c == ')') paren = Math.max(0, paren - 1);
                else if (c == ',' && paren == 0) {
                    parts.add(cur.toString());
                    cur.setLength(0);
                    continue;
                }
            }
            cur.append(c);
        }
        if (!cur.isEmpty()) parts.add(cur.toString());
        return parts;
    }


    private static class ColumnDef {
        String name;
        String type;
        boolean nullable = true;
        String defaultValue;


        @Override
        public String toString() {
            return "ColumnDef{" +
                    "name='" + name + '\'' +
                    ", type='" + type + '\'' +
                    ", nullable=" + nullable +
                    ", defaultValue='" + defaultValue + '\'' +
                    '}';
        }
    }

}
